package cn.jtfadmin.activiti.service.impl;

import cn.jtfadmin.activiti.domain.dto.*;
import cn.jtfadmin.activiti.domain.entity.FormDesignEntity;
import cn.jtfadmin.activiti.repository.FormDesignRepository;
import cn.jtfadmin.activiti.service.ActModelService;
import cn.jtfadmin.base.dict.dto.DictItemModelDTO;
import cn.jtfadmin.base.filesystem.config.FileSystem;
import cn.jtfadmin.base.lang.common.domain.dto.PageRDTO;
import cn.jtfadmin.base.lang.common.event.EntityEvent;
import cn.jtfadmin.base.lang.common.utils.AkUtils;
import cn.jtfadmin.base.lang.common.utils.CompressUtils;
import cn.jtfadmin.base.lang.common.utils.IdGenUtils;
import cn.jtfadmin.base.lang.enums.BooleanEnum;
import cn.jtfadmin.base.lang.enums.EntityEventEnum;
import cn.jtfadmin.core.modules.sys.domain.entity.SysDepartEntity;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.activiti.bpmn.converter.BpmnXMLConverter;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.editor.constants.ModelDataJsonConstants;
import org.activiti.editor.language.json.converter.BpmnJsonConverter;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.impl.util.io.InputStreamSource;
import org.activiti.engine.repository.*;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.PNGTranscoder;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.SystemUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;

/**
 * actModel服务实现类
 * @author jtf
 * @since 0.0.2
 */
@Service
public class ActModelServiceImpl implements ActModelService, ApplicationListener<EntityEvent> {


    @Autowired
    private ObjectMapper objectMapper;


    @Autowired
    private RepositoryService repositoryService;

    @Autowired
    private FileSystem fileSystem;

    @Autowired
    private FormDesignRepository formDesignRepository;



    @Override
    @Transactional
    public Model addModel(ModelAddPDTO addPDTO) {
        // 名称
        String name = addPDTO.getName();
        // 描述
        String describeInfo = addPDTO.getDescribeInfo();
        // 新增model
        Model model = repositoryService.newModel();
        ObjectNode modelNode = objectMapper.createObjectNode();
        modelNode.put(ModelDataJsonConstants.MODEL_NAME, name);
        modelNode.put(ModelDataJsonConstants.MODEL_DESCRIPTION, describeInfo);
        model.setName(name);
        model.setMetaInfo(modelNode.toString());
        model.setTenantId(addPDTO.getTenantId());
        repositoryService.saveModel(model);
        doEditModel(model, addPDTO);
        return model;
    }

    @Override
    @Transactional
    public Model editModel(ModelEditPDTO editPDTO) {
        Model model = repositoryService.createModelQuery().modelId(editPDTO.getId()).singleResult();
        doEditModel(model, editPDTO);
        return model;
    }



    @Override
    public PageRDTO<ModelDTO> listModel(String name, String tenantId, int pageNo, int pageSize) {
        ModelQuery modelQuery = repositoryService.createModelQuery();
        if(StringUtils.hasText(name)){
            modelQuery.modelNameLike(name);
        }
        modelQuery.modelTenantId(tenantId);
        long count = modelQuery.count();
        modelQuery = repositoryService.createModelQuery();
        if(StringUtils.hasText(name)){
            modelQuery.modelNameLike(name);
        }
        modelQuery.modelTenantId(tenantId);
        modelQuery.orderByCreateTime().desc();
        List<Model> list = modelQuery.listPage((pageNo-1)*pageSize, pageSize);
        List<ModelDTO> collect = list.stream().map(t -> {
            ModelDTO dto = new ModelDTO();
            dto.setId(t.getId());
            String metaInfo = t.getMetaInfo();
            try {
                JsonNode modelNode = objectMapper.readTree(metaInfo);
                dto.setDescribeInfo(modelNode.get(ModelDataJsonConstants.MODEL_DESCRIPTION).asText(""));
            } catch (JsonProcessingException e) {
                // ignore
            }

            dto.setKey(t.getKey());
            dto.setName(t.getName());
            dto.setVersion(t.getVersion());
            dto.setDeploymentId(t.getDeploymentId());
            Date createTime = t.getCreateTime();
            dto.setCreateDateTime(LocalDateTime.ofInstant(createTime.toInstant(), ZoneId.systemDefault()));
            dto.setTenantId(t.getTenantId());
            return dto;
        }).collect(Collectors.toList());
        return new PageRDTO<>(collect, count);
    }

    @Override
    @Transactional
    public void deleteModel(String id) {
        repositoryService.deleteModel(id);
        List<Deployment> list = repositoryService.createDeploymentQuery().deploymentKey(genDeploymentKey(id)).list();
        for (Deployment deployment : list) {
            deleteDeployment(deployment.getId());
        }
        deleteFormDesignByModelId(id);

    }

    @Override
    @Transactional
    public void deleteBatchModel(String ids) {
        String[] idArray = StringUtils.delimitedListToStringArray(ids, ",");
        for (String id : idArray) {
            if(StringUtils.hasText(id)){
                deleteModel(id);
            }
        }
    }

    @Override
    @Transactional
    public void deployModel(String id, String file) {
        // 获取模型
        Model modelData = repositoryService.getModel(id);
        byte[] bytes = repositoryService.getModelEditorSource(modelData.getId());

        if (bytes == null) {
            throw new IllegalArgumentException("模型数据为空，请先成功设计流程并保存");
        }

        try {
            JsonNode modelNode = new ObjectMapper().readTree(bytes);

            BpmnModel model = new BpmnJsonConverter().convertToBpmnModel(modelNode);
            if(model.getProcesses().size()==0){
                throw new IllegalArgumentException("模型不符要求，请至少设计一条主线流程");
            }
            byte[] bpmnBytes = new BpmnXMLConverter().convertToXML(model);

            // 部署发布模型流程
            String processName = modelData.getName() + ".bpmn20.xml";
            String key = genDeploymentKey(modelData.getId());
            DeploymentBuilder deploymentBuilder = repositoryService.createDeployment()
                    .key(key)
                    .addString(processName, new String(bpmnBytes, "UTF-8"));

            byte[] modelEditorSourceExtra = repositoryService.getModelEditorSourceExtra(modelData.getId());
            if(modelEditorSourceExtra != null){
                String pngName = modelData.getName()+".png";
                deploymentBuilder.addBytes(pngName, modelEditorSourceExtra);
            }
            if(StringUtils.hasText(file)){
                String[] strings = StringUtils.delimitedListToStringArray(file, ",");
                for (String string : strings) {
                    string = string.trim();
                    String fileName = fileSystem.getFileName(string);
                    try (InputStream inputStream = fileSystem.getFileInputStream(string)) {
                        deploymentBuilder.addBytes(fileName, StreamUtils.copyToByteArray(inputStream));

                    } catch (IOException e1) {
                        throw new RuntimeException(e1);
                    }
                }

            }
            List<FormDesignEntity> formDesignEntities = listFormDesign(modelData.getId());
            for (FormDesignEntity formDesignEntity : formDesignEntities) {
                String content = formDesignEntity.getContent();
                if(content == null){
                    content = "";
                }
                deploymentBuilder.addString(formDesignEntity.getName(), content);
            }

            List<Deployment> list = repositoryService.createDeploymentQuery().deploymentKey(key).list();
            Integer version = 1;
            List<Integer> versions = list.stream().map(t->Integer.valueOf(t.getName())).collect(Collectors.toList());
            Collections.sort(versions);
            Collections.reverse(versions);
            if(!CollectionUtils.isEmpty(versions)){
                version = versions.get(0)+1;
            }
            deploymentBuilder.tenantId(modelData.getTenantId());
            deploymentBuilder.name(version.toString());
            Deployment deploy = deploymentBuilder.deploy();
            String deployId = deploy.getId();
            // 设置部署模型，增加版本
            modelData.setDeploymentId(deployId);
            modelData.setVersion(version);
            repositoryService.saveModel(modelData);
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    @Override
    @Transactional
    public void deleteDeployment(String id) {
        repositoryService.deleteDeployment(id, true);
    }

    @Override
    @Transactional
    public void deleteBatchDeployment(String ids) {
        String[] idArray = StringUtils.delimitedListToStringArray(ids, ",");
        for (String id : idArray) {
            if(StringUtils.hasText(id)){
                deleteDeployment(id);
            }
        }
    }

    @Override
    public Set<String> listDeploymentIdsByModelId(String modelId){
        List<Deployment> deploymentList = repositoryService.createDeploymentQuery().deploymentKey(genDeploymentKey(modelId))
                .orderByDeploymenTime().desc().list();
        Set<String> deploymentIds = deploymentList.stream().map(t->t.getId()).collect(Collectors.toSet());
        return deploymentIds;
    }

    @Override
    public List<ProcessDefinitionDTO> listDefinition(String modelId) {
        List<Deployment> deploymentList = repositoryService.createDeploymentQuery().deploymentKey(genDeploymentKey(modelId))
                .orderByDeploymenTime().desc().list();
        Set<String> deploymentIds = deploymentList.stream().map(t->t.getId()).collect(Collectors.toSet());


        ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
        processDefinitionQuery.deploymentIds(deploymentIds);
        List<ProcessDefinition> processDefinitionList = processDefinitionQuery
                .orderByDeploymentId()
                .orderByProcessDefinitionVersion().desc()
                .list();
        return processDefinitionList
                .stream()
                .map(t->{
                    ProcessDefinitionDTO dto = new ProcessDefinitionDTO();
                    dto.setId(t.getId());
                    dto.setKey(t.getKey());
                    dto.setName(t.getName());
                    dto.setDescription(t.getDescription());
                    Optional<Deployment> optional = deploymentList
                            .stream()
                            .filter(v -> t.getDeploymentId().equals(v.getId()))
                            .findFirst();
                    dto.setVersion(
                            Integer.valueOf(
                                    optional.isPresent()?optional.get().getName():"0"
                            )
                    );
                    dto.setDeploymentId(t.getDeploymentId());
                    dto.setHasStartFormKey(t.hasStartFormKey());
                    if(t.hasGraphicalNotation()){
                        dto.setDiagramResourceName(t.getDiagramResourceName());
                    }
                    dto.setSuspended(t.isSuspended() ? BooleanEnum.TRUE : BooleanEnum.FALSE);
                    return dto;
                })
                .sorted((t1, t2)->Integer.compare(t2.getVersion(), t1.getVersion()))
                .collect(Collectors.toList());
    }

    @Override
    @Transactional
    public void deleteDefinition(String id) {
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
                .processDefinitionId(id)
                .singleResult();
        String deploymentId = processDefinition.getDeploymentId();
        Deployment deployment = repositoryService.createDeploymentQuery().deploymentId(deploymentId).singleResult();
        String key = deployment.getKey();
        String modelId = praseModelId(key);
        Integer modelVersion = null;

        Model model1 = repositoryService.createModelQuery().modelId(modelId).singleResult();
        if(model1 != null){
            modelVersion = model1.getVersion();
        }
        deleteDeployment(deploymentId);
        Model model = repositoryService.createModelQuery().modelId(modelId).singleResult();
        if(Objects.nonNull(model) && Objects.equals(model.getVersion(), modelVersion)){
            model.setVersion(0);
        }

        repositoryService.saveModel(model);
    }

    @Override
    @Transactional
    public void updateModelVersion(String id, Integer version) {
        Deployment deployment = repositoryService.createDeploymentQuery().deploymentKey(genDeploymentKey(id))
                .deploymentName(version.toString()).singleResult();
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deployment.getId()).singleResult();

        BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinition.getId());
        ObjectNode editorNode = new BpmnJsonConverter().convertToJson(bpmnModel);
        repositoryService.addModelEditorSource(id, editorNode.toString().getBytes(StandardCharsets.UTF_8));
        try(InputStream is = repositoryService.getProcessDiagram(processDefinition.getId())){
            if(is != null){
                repositoryService.addModelEditorSourceExtra(id, StreamUtils.copyToByteArray(is));
            }
        }catch (Exception e){
            throw new RuntimeException(e);
        }
        deleteFormDesignByModelId(id);
        List<String> deploymentResourceNames = repositoryService.getDeploymentResourceNames(deployment.getId());
        for (String deploymentResourceName : deploymentResourceNames) {
            if(deploymentResourceName.endsWith(".bpmn20.xml")||deploymentResourceName.endsWith(".png")){
                continue;
            }
            addFormDesign(new FormDesignAddPDTO(deploymentResourceName, id, ""));

        }

        Model model = repositoryService.createModelQuery().modelId(id).singleResult();
        model.setVersion(version);
        model.setDeploymentId(deployment.getId());
        repositoryService.saveModel(model);
    }


    @Override
    public byte[] getModelEditorSourceExtra(String id){
        byte[] modelEditorSourceExtra = repositoryService.getModelEditorSourceExtra(id);
        return modelEditorSourceExtra;
    }


    @Override
    public InputStream getProcessResouce(String deploymentId, String name){
        return repositoryService.getResourceAsStream(deploymentId, name);
    }



    @Override
    public void exportResouce(String ids, OutputStream outputStream){
        String[] strings = StringUtils.delimitedListToStringArray(ids, ",");
        File javaIoTmpDir = SystemUtils.getJavaIoTmpDir();
        String dirName = IdGenUtils.md5Gen();
        File temDir = new File(javaIoTmpDir, dirName);
        if(temDir.exists()){
            try {
                FileUtils.forceDeleteOnExit(temDir);
            } catch (IOException e) {
                //
            }
        }
        temDir.mkdirs();


        for (String string : strings) {
            if(StringUtils.hasText(string)){

                ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(string).singleResult();
                List<String> deploymentResourceNames = repositoryService.getDeploymentResourceNames(processDefinition.getDeploymentId());
                File f = new File(temDir, repositoryService.createDeploymentQuery().deploymentId(
                        processDefinition.getDeploymentId()
                ).singleResult().getName());
                f.mkdirs();
                for (String deploymentResourceName : deploymentResourceNames) {
                    try(InputStream resourceAsStream =
                                repositoryService.getResourceAsStream(processDefinition.getDeploymentId(), deploymentResourceName);
                    ){
                        File f1 = new File(f, deploymentResourceName);

                        FileUtils.writeByteArrayToFile(f1, StreamUtils.copyToByteArray(resourceAsStream));
                    }catch (Exception e){
                        throw new RuntimeException(e);
                    }
                }
            }
        }
        File zipFile = new File(javaIoTmpDir, IdGenUtils.md5Gen());
        try {
            FileUtils.forceDeleteOnExit(zipFile);
        } catch (IOException e) {
            // ignore
        }
        try {
            zipFile.createNewFile();
        } catch (IOException e) {
            //
        }
        CompressUtils.compressZip(zipFile, temDir);
        try(InputStream ins = new FileInputStream(zipFile)){
            StreamUtils.copy(ins, outputStream);
        }catch (Exception e){
            throw new IllegalArgumentException(e);
        }
        try {
            FileUtils.forceDeleteOnExit(zipFile);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            FileUtils.forceDeleteOnExit(temDir);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    protected void doEditModel(Model model, ModelAddPDTO addPDTO){
        // 名称
        String name = addPDTO.getName();
        // 描述
        String describeInfo = addPDTO.getDescribeInfo();
        ObjectNode modelJson = null;
        try {
            modelJson = (ObjectNode) objectMapper.readTree(model.getMetaInfo());
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        modelJson.put(MODEL_NAME, name);
        modelJson.put(MODEL_DESCRIPTION, describeInfo);
        model.setMetaInfo(modelJson.toString());
        model.setName(describeInfo);
        model.setTenantId(addPDTO.getTenantId());
        String id = model.getId();


        ObjectNode editorNode = null;
        if(StringUtils.hasText(addPDTO.getProcessBpmn())){
            try (InputStream fileInputStream = fileSystem.getFileInputStream(addPDTO.getProcessBpmn())){
                BpmnModel bpmnModel = new BpmnXMLConverter()
                        .convertToBpmnModel(new InputStreamSource(fileInputStream), true, true);
                editorNode = new BpmnJsonConverter().convertToJson(bpmnModel);
            } catch (Exception e) {
                throw new IllegalArgumentException("流程图转换错误", e);
            }
        }else {
            //完善ModelEditorSource
            editorNode = objectMapper.createObjectNode();
            editorNode.put("id", "canvas");
            editorNode.put("resourceId", "canvas");
            ObjectNode stencilSetNode = objectMapper.createObjectNode();
            stencilSetNode.put("namespace",
                    "http://b3mn.org/stencilset/bpmn2.0#");
            ObjectNode propertiesNode = objectMapper.createObjectNode();
            propertiesNode.put("name", name);
            propertiesNode.put("documentation", describeInfo);
            editorNode.set("properties", propertiesNode);
            editorNode.set("stencilset", stencilSetNode);
        }
        try {
            repositoryService.addModelEditorSource(id, editorNode.toString().getBytes("utf-8"));
        } catch (UnsupportedEncodingException e) {
            // ingore
        }
        if(StringUtils.hasText(addPDTO.getProccessImg())){
            try (InputStream fileInputStream = fileSystem.getFileInputStream(addPDTO.getProccessImg())){
                repositoryService.addModelEditorSourceExtra(id, StreamUtils.copyToByteArray(fileInputStream));
            } catch (Exception e) {
                throw new IllegalArgumentException("流程图转换错误", e);
            }
        }else {
            try ( InputStream svgStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("default_svg.xml");
                  ByteArrayOutputStream outStream = new ByteArrayOutputStream()){
                TranscoderInput input = new TranscoderInput(svgStream);
                PNGTranscoder transcoder = new PNGTranscoder();
                TranscoderOutput output = new TranscoderOutput(outStream);
                // Do the transformation
                transcoder.transcode(input, output);
                final byte[] result = outStream.toByteArray();
                repositoryService.addModelEditorSourceExtra(id, result);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

    }




    @Override
    @Transactional
    public FormDesignEntity addFormDesign(FormDesignAddPDTO addPDTO){
        FormDesignEntity entity = new FormDesignEntity();
        AkUtils.copyProperties(addPDTO, entity);
        entity = formDesignRepository.save(entity);
        return entity;
    }

    @Override
    @Transactional
    public FormDesignEntity editFormDesign(FormDesignEditPDTO editPDTO){
        FormDesignEntity entity = formDesignRepository.findById(editPDTO.getId()).get();
        AkUtils.copyProperties(editPDTO, entity);
        entity = formDesignRepository.save(entity);
        return entity;
    }

    @Override
    @Transactional
    public void deleteFormDesign(String id){
        formDesignRepository.deleteById(id);
    }

    @Override
    @Transactional
    public void deleteBatchFormDesign(String ids){
        String[] idArray = StringUtils.delimitedListToStringArray(ids, ",");
        for (String id : idArray) {
            if(StringUtils.hasText(id)){
                deleteFormDesign(id);
            }
        }
    }


    private void deleteFormDesignByModelId(String modelId){
        List<FormDesignEntity> formDesignEntities = listFormDesign(modelId);
        for (FormDesignEntity formDesignEntity : formDesignEntities) {
            formDesignRepository.delete(formDesignEntity);
        }
    }

    @Override
    public List<FormDesignEntity> listFormDesign(String modelId){
        return formDesignRepository.findAll((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("modelId"), modelId),
                Sort.by(Sort.Direction.DESC, "createDateTime"));
    }

    @Override
    public Optional<FormDesignEntity> getFormDesignById(String id) {
        return formDesignRepository.findById(id);
    }

    @Override
    @Transactional
    public void updateFormDesignContent(String id, String content) {
        FormDesignEntity entity = formDesignRepository.findById(id).get();
        entity.setContent(content);
        formDesignRepository.save(entity);
    }


    @Override
    public String genDeploymentKey(String modelId){
        return "deployment_"+modelId;
    }

    @Override
    public String praseModelId(String deploymentKey){
        if(deploymentKey.startsWith("deployment_")){
            return deploymentKey.substring("deployment_".length());
        }
        throw new IllegalArgumentException("解析模型id错误");
    }

    @Override
    public List<DictItemModelDTO> modelNamed(String tenantId) {
        List<Model> list = repositoryService.createModelQuery().modelTenantId(tenantId).list();
        return list.stream().map(t->{
            DictItemModelDTO dto = new DictItemModelDTO();
            dto.setText(t.getName());
            dto.setValue(t.getId());
            return dto;
        }).collect(Collectors.toList());
    }

    @Override
    @Transactional
    public void onApplicationEvent(EntityEvent event) {
        Object entity = event.getEntity();
        if(entity instanceof SysDepartEntity && EntityEventEnum.POST_REMOVE.equals( event.getType())){
            SysDepartEntity sysDepartEntity = SysDepartEntity.class.cast(entity);
            List<Model> list = repositoryService.createModelQuery().modelTenantId(sysDepartEntity.getId())
                    .list();
            for (Model model : list) {
                deleteModel(model.getId());
            }
        }
    }
}
